/*
 * Adapted from the Wizardry License
 *
 * Copyright (c) 2016-2020 larryTheCoder and contributors
 *
 * Permission is hereby granted to any persons and/or organizations
 * using this software to copy, modify, merge, publish, and distribute it.
 * Said persons and/or organizations are not allowed to use the software or
 * any derivatives of the work for commercial use or any other means to generate
 * income, nor are they allowed to claim this software as their own.
 *
 * The persons and/or organizations are also disallowed from sub-licensing
 * and/or trademarking this software without explicit permission from larryTheCoder.
 *
 * Any persons and/or organizations using this software must disclose their
 * source code and have it publicly available, include this license,
 * provide sufficient credit to the original authors of the project (IE: larryTheCoder),
 * as well as provide a link to the original project.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR
 * PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.larryTheCoder.utils.json;

import java.util.Iterator;

/**
 * This provides static methods to convert an XML text into a JSONArray or JSONObject, and to covert a JSONArray or
 * JSONObject into an XML text using the JsonML transform.
 *
 * @author JSON.org
 * @version 2014-05-03
 */
class JSONML {
    /**
     * Parse XML values and store them in a JSONArray.
     *
     * @param x         The XMLTokener containing the source string.
     * @param arrayForm true if array form, false if object form.
     * @param ja        The JSONArray that is containing the current tag or null if we are at the outermost level.
     * @return A JSONArray if the value is the outermost tag, otherwise null.
     * @throws JSONException
     */
    private static Object parse(XMLTokener x, boolean arrayForm, JSONArray ja) throws JSONException {
        String attribute;
        char c;
        String closeTag = null;
        int i;
        JSONArray newja = null;
        JSONObject newjo = null;
        Object token;
        String tagName = null;
        // Test for and skip past these forms:
        // <!-- ... -->
        // <![ ... ]]>
        // <! ... >
        // <? ... ?>
        while (true) {
            if (!x.more()) {
                throw x.syntaxError("Bad XML");
            }
            token = x.nextContent();
            if (token == XML.LT) {
                token = x.nextToken();
                if (token instanceof Character) {
                    if (token == XML.SLASH) {
                        // Close tag </
                        token = x.nextToken();
                        if (!(token instanceof String)) {
                            throw new JSONException("Expected a closing name instead of '" + token + "'.");
                        }
                        if (x.nextToken() != XML.GT) {
                            throw x.syntaxError("Misshaped close tag");
                        }
                        return token;
                    } else if (token == XML.BANG) {
                        // <!
                        c = x.next();
                        if (c == '-') {
                            if (x.next() == '-') {
                                x.skipPast("-->");
                            } else {
                                x.back();
                            }
                        } else if (c == '[') {
                            token = x.nextToken();
                            if (token.equals("CDATA") && (x.next() == '[')) {
                                if (ja != null) {
                                    ja.put(x.nextCDATA());
                                }
                            } else {
                                throw x.syntaxError("Expected 'CDATA['");
                            }
                        } else {
                            i = 1;
                            do {
                                token = x.nextMeta();
                                if (token == null) {
                                    throw x.syntaxError("Missing '>' after '<!'.");
                                } else if (token == XML.LT) {
                                    i += 1;
                                } else if (token == XML.GT) {
                                    i -= 1;
                                }
                            } while (i > 0);
                        }
                    } else if (token == XML.QUEST) {
                        // <?
                        x.skipPast("?>");
                    } else {
                        throw x.syntaxError("Misshaped tag");
                    }
                    // Open tag <
                } else {
                    if (!(token instanceof String)) {
                        throw x.syntaxError("Bad tagName '" + token + "'.");
                    }
                    tagName = (String) token;
                    newja = new JSONArray();
                    newjo = new JSONObject();
                    if (arrayForm) {
                        newja.put(tagName);
                        if (ja != null) {
                            ja.put(newja);
                        }
                    } else {
                        newjo.put("tagName", tagName);
                        if (ja != null) {
                            ja.put(newjo);
                        }
                    }
                    token = null;
                    for (; ; ) {
                        if (token == null) {
                            token = x.nextToken();
                        }
                        if (!(token instanceof String)) {
                            break;
                        }
                        // attribute = value
                        attribute = (String) token;
                        if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) {
                            throw x.syntaxError("Reserved attribute.");
                        }
                        token = x.nextToken();
                        if (token == XML.EQ) {
                            token = x.nextToken();
                            if (!(token instanceof String)) {
                                throw x.syntaxError("Missing value");
                            }
                            newjo.accumulate(attribute, XML.stringToValue((String) token));
                            token = null;
                        } else {
                            newjo.accumulate(attribute, "");
                        }
                    }
                    if (arrayForm && (newjo.length() > 0)) {
                        newja.put(newjo);
                    }
                    // Empty tag <.../>
                    if (token == XML.SLASH) {
                        if (x.nextToken() != XML.GT) {
                            throw x.syntaxError("Misshaped tag");
                        }
                        if (ja == null) {
                            if (arrayForm) {
                                return newja;
                            } else {
                                return newjo;
                            }
                        }
                        // Content, between <...> and </...>
                    } else {
                        if (token != XML.GT) {
                            throw x.syntaxError("Misshaped tag");
                        }
                        closeTag = (String) parse(x, arrayForm, newja);
                        if (closeTag != null) {
                            if (!closeTag.equals(tagName)) {
                                throw x.syntaxError("Mismatched '" + tagName + "' and '" + closeTag + "'");
                            }
                            tagName = null;
                            if (!arrayForm && (newja.length() > 0)) {
                                newjo.put("childNodes", newja);
                            }
                            if (ja == null) {
                                if (arrayForm) {
                                    return newja;
                                } else {
                                    return newjo;
                                }
                            }
                        }
                    }
                }
            } else {
                if (ja != null) {
                    ja.put(token instanceof String ? XML.stringToValue((String) token) : token);
                }
            }
        }
    }

    /**
     * Convert a well-formed (but not necessarily valid) XML string into a JSONArray using the JsonML transform. Each
     * XML tag is represented as a JSONArray in which the first element is the tag name. If the tag has attributes, then
     * the second element will be JSONObject containing the name/value pairs. If the tag contains children, then strings
     *
     * @param string The source string.
     * @return A JSONArray containing the structured data from the XML string.
     * @throws JSONException
     */
    public static JSONArray toJSONArray(String string) throws JSONException {
        return toJSONArray(new XMLTokener(string));
    }

    /**
     * Convert a well-formed (but not necessarily valid) XML string into a JSONArray using the JsonML transform. Each
     * XML tag is represented as a JSONArray in which the first element is the tag name. If the tag has attributes, then
     * the second element will be JSONObject containing the name/value pairs. If the tag contains children, then strings
     * and JSONArrays will represent the child content and tags. Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code>
     * are ignored.
     *
     * @param x An XMLTokener.
     * @return A JSONArray containing the structured data from the XML string.
     * @throws JSONException
     */
    private static JSONArray toJSONArray(XMLTokener x) throws JSONException {
        return (JSONArray) parse(x, true, null);
    }

    /**
     * Convert a well-formed (but not necessarily valid) XML string into a JSONObject using the JsonML transform. Each
     * XML tag is represented as a JSONObject with a "tagName" property. If the tag has attributes, then the attributes
     * will be in the JSONObject as properties. If the tag contains children, the object will have a "childNodes"
     * property which will be an array of strings and JsonML JSONObjects.
     * <p>
     * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
     *
     * @param x An XMLTokener of the XML source text.
     * @return A JSONObject containing the structured data from the XML string.
     * @throws JSONException
     */
    private static JSONObject toJSONObject(XMLTokener x) throws JSONException {
        return (JSONObject) parse(x, false, null);
    }

    /**
     * Convert a well-formed (but not necessarily valid) XML string into a JSONObject using the JsonML transform. Each
     * XML tag is represented as a JSONObject with a "tagName" property. If the tag has attributes, then the attributes
     * will be in the JSONObject as properties. If the tag contains children, the object will have a "childNodes"
     * property which will be an array of strings and JsonML JSONObjects.
     * <p>
     * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
     *
     * @param string The XML source text.
     * @return A JSONObject containing the structured data from the XML string.
     * @throws JSONException
     */
    public static JSONObject toJSONObject(String string) throws JSONException {
        return toJSONObject(new XMLTokener(string));
    }

    /**
     * Reverse the JSONML transformation, making an XML text from a JSONArray.
     *
     * @param ja A JSONArray.
     * @return An XML string.
     * @throws JSONException
     */
    private static String toString(JSONArray ja) throws JSONException {
        int i;
        JSONObject jo;
        String key;
        Iterator<String> keys;
        int length;
        Object object;
        StringBuilder sb = new StringBuilder();
        String tagName;
        String value;
        // Emit <tagName
        tagName = ja.getString(0);
        XML.noSpace(tagName);
        tagName = XML.escape(tagName);
        sb.append('<');
        sb.append(tagName);
        object = ja.opt(1);
        if (object instanceof JSONObject) {
            i = 2;
            jo = (JSONObject) object;
            // Emit the attributes
            keys = jo.keys();
            while (keys.hasNext()) {
                key = keys.next();
                XML.noSpace(key);
                value = jo.optString(key);
                if (value != null) {
                    sb.append(' ');
                    sb.append(XML.escape(key));
                    sb.append('=');
                    sb.append('"');
                    sb.append(XML.escape(value));
                    sb.append('"');
                }
            }
        } else {
            i = 1;
        }
        // Emit content in body
        length = ja.length();
        if (i >= length) {
            sb.append('/');
            sb.append('>');
        } else {
            sb.append('>');
            do {
                object = ja.get(i);
                i += 1;
                if (object != null) {
                    if (object instanceof String) {
                        sb.append(XML.escape(object.toString()));
                    } else if (object instanceof JSONObject) {
                        sb.append(toString((JSONObject) object));
                    } else if (object instanceof JSONArray) {
                        sb.append(toString((JSONArray) object));
                    }
                }
            } while (i < length);
            sb.append('<');
            sb.append('/');
            sb.append(tagName);
            sb.append('>');
        }
        return sb.toString();
    }

    /**
     * Reverse the JSONML transformation, making an XML text from a JSONObject. The JSONObject must contain a "tagName"
     * property. If it has children, then it must have a "childNodes" property containing an array of objects. The other
     * properties are attributes with string values.
     *
     * @param jo A JSONObject.
     * @return An XML string.
     * @throws JSONException
     */
    private static String toString(JSONObject jo) throws JSONException {
        StringBuilder sb = new StringBuilder();
        int i;
        JSONArray ja;
        String key;
        Iterator<String> keys;
        int length;
        Object object;
        String tagName;
        String value;
        // Emit <tagName
        tagName = jo.optString("tagName");
        if (tagName == null) {
            return XML.escape(jo.toString());
        }
        XML.noSpace(tagName);
        tagName = XML.escape(tagName);
        sb.append('<');
        sb.append(tagName);
        // Emit the attributes
        keys = jo.keys();
        while (keys.hasNext()) {
            key = keys.next();
            if (!"tagName".equals(key) && !"childNodes".equals(key)) {
                XML.noSpace(key);
                value = jo.optString(key);
                if (value != null) {
                    sb.append(' ');
                    sb.append(XML.escape(key));
                    sb.append('=');
                    sb.append('"');
                    sb.append(XML.escape(value));
                    sb.append('"');
                }
            }
        }
        // Emit content in body
        ja = jo.optJSONArray("childNodes");
        if (ja == null) {
            sb.append('/');
            sb.append('>');
        } else {
            sb.append('>');
            length = ja.length();
            for (i = 0; i < length; i += 1) {
                object = ja.get(i);
                if (object != null) {
                    if (object instanceof String) {
                        sb.append(XML.escape(object.toString()));
                    } else if (object instanceof JSONObject) {
                        sb.append(toString((JSONObject) object));
                    } else if (object instanceof JSONArray) {
                        sb.append(toString((JSONArray) object));
                    } else {
                        sb.append(object.toString());
                    }
                }
            }
            sb.append('<');
            sb.append('/');
            sb.append(tagName);
            sb.append('>');
        }
        return sb.toString();
    }
}
